﻿//-----------------------------------------------------------------------
// <copyright file="BlobRepository.cs" company="OOHM PROCESSAMENTO DE DADOS LTDA.">
//     Microsoft Public License (MS-PL) This license governs use of the accompanying
//     software. If you use the software, you accept this license. If you do not
//     accept the license, do not use the software. 1. Definitions The terms "reproduce,"
//     "reproduction," "derivative works," and "distribution" have the same meaning
//     here as under U.S. copyright law. A "contribution" is the original software,
//     or any additions or changes to the software. A "contributor" is any person
//     that distributes its contribution under this license. "Licensed patents"
//     are a contributor's patent claims that read directly on its contribution.
//     2. Grant of Rights (A) Copyright Grant- Subject to the terms of this license,
//     including the license conditions and limitations in section 3, each contributor
//     grants you a non-exclusive, worldwide, royalty-free copyright license to
//     reproduce its contribution, prepare derivative works of its contribution,
//     and distribute its contribution or any derivative works that you create.
//     (B) Patent Grant- Subject to the terms of this license, including the license
//     conditions and limitations in section 3, each contributor grants you a non-exclusive,
//     worldwide, royalty-free license under its licensed patents to make, have
//     made, use, sell, offer for sale, import, and/or otherwise dispose of its
//     contribution in the software or derivative works of the contribution in
//     the software. 3. Conditions and Limitations (A) No Trademark License- This
//     license does not grant you rights to use any contributors' name, logo, or
//     trademarks. (B) If you bring a patent claim against any contributor over
//     patents that you claim are infringed by the software, your patent license
//     from such contributor to the software ends automatically. (C) If you distribute
//     any portion of the software, you must retain all copyright, patent, trademark,
//     and attribution notices that are present in the software. (D) If you distribute
//     any portion of the software in source code form, you may do so only under
//     this license by including a complete copy of this license with your distribution.
//     If you distribute any portion of the software in compiled or object code
//     form, you may only do so under a license that complies with this license.
//     (E) The software is licensed "as-is." You bear the risk of using it. The
//     contributors give no express warranties, guarantees or conditions. You may
//     have additional consumer rights under your local laws which this license
//     cannot change. To the extent permitted under your local laws, the contributors
//     exclude the implied warranties of merchantability, fitness for a particular
// </copyright>
//-----------------------------------------------------------------------
namespace AbstractSaaSTemplate.Infrastructure.Win2K8WithSQLServer.Repositories{    using System;
    using System.IO;
    using System.Linq;
    using System.Text;
    using AbstractSaaSTemplate.Domain.Models.Entities;
    using AbstractSaaSTemplate.Domain.Repositories;
    using AbstractSaaSTemplate.Infrastructure.Win2K8WithSQLServer;

    /// <summary>
    /// Implements 'IBlobsRepository' and all its contract, providing
    /// data accessibility to the aggregate 'Blobs'.
    /// </summary>
    public sealed class BlobRepository : SQLServerConnectedRepositoryBase, IBlobRepository
    {
        /// <summary>
        /// Initializes a new instance of the BlobRepository class.
        /// </summary>
        /// <param name="loaderForConnectionString">The delegate capable of retrieving the connection string for the EF4 context.</param>
        /// <param name="loaderForRootPath">The delegate capable of retrieving the root directory path for the system blobs.</param>
        /// <param name="loaderForRootUrl">The delegate capable of retrieving the root url for the system blobs.</param>
        public BlobRepository(
            Func<string> loaderForConnectionString,
            Func<string> loaderForRootPath,
            Func<Uri> loaderForRootUrl)
            : base(loaderForConnectionString)
        {
            if (loaderForRootPath == null)
            {
                throw new ArgumentNullException("loaderForRootPath");
            }

            if (loaderForRootUrl == null)
            {
                throw new ArgumentNullException("loaderForRootUrl");
            }
            
            this.LoaderRootPath = loaderForRootPath;
            this.LoaderRootUrl = loaderForRootUrl;
        }

        /// <summary>
        /// Gets the root path for the blobs directory.
        /// </summary>
        public Func<string> LoaderRootPath { get; private set; }

        /// <summary>
        /// Gets the root uri for the blobs repository.
        /// </summary>
        public Func<Uri> LoaderRootUrl { get; private set; }

        /// <summary>
        /// Creates or update a blob.
        /// </summary>
        /// <param name="entity">The blob which will be created or updated.</param>
        public void AddOrUpdateBlob(Blob entity)
        {
            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }
            
            // first we check if there is an Id defined to the blob entity
            // if there is none, first thing we have to do is to generate
            // such id since that is used for discovering the folder.
            if (entity.Id == Guid.Empty)
            {
                entity.Id = Guid.NewGuid();
            }

            // we retrieve the directory from the this.RootPathGetter
            var directoryName = this.LoaderRootPath();
            if (!Directory.Exists(directoryName))
            {
                Directory.CreateDirectory(directoryName);
            }
            
            // we discover the folder from the id of the entity
            var directory = new DirectoryInfo(directoryName);
            var folderName = GetBaseFolderForId(entity.Id);
            var folderPath = Path.Combine(
                directory.FullName,
                folderName);

            // of course, if there is no folder, we attempt to create it
            var folder = null as DirectoryInfo;
            if (!Directory.Exists(folderPath))
            {
                Directory.CreateDirectory(folderPath);
            }

            folder = new DirectoryInfo(folderPath);

            // finally, we will check if the file already exists in the folder
            var fileName = string.Concat(entity.Id, Path.GetExtension(entity.OriginalName));
            var filePath = Path.Combine(
                folder.FullName,
                fileName);

            // here we operate the file system creating or modifying the blob
            CreateOrModifyFile(filePath, entity.Stream);

            // to keep integrity and queryability, we will also register the
            // blob into the database using the entity framework 4.
            this.AddOrUpdateDatabaseEntry(filePath, entity);
        }

        /// <summary>
        /// Retrieves a blob from its id.
        /// </summary>
        /// <param name="id">The id of the blob.</param>
        /// <returns>Returns an instance of Blob.</returns>
        public Blob GetBlobById(Guid id)
        {
            using (var context = NewContext())
            {
                var blob = context.Blobs
                    .Where(d => d.Id == id)
                    .FirstOrDefault();

                if (blob == null)
                {
                    return null;
                }

                var entity = new Blob();
                entity.Id = blob.Id;
                CopyFromEF4ToDomain<Core.Blob, Blob>(blob, entity);

                // here we shall read the stream containing the data of the file.
                var memoryStream = null as MemoryStream;
                using (var stream = File.OpenRead(blob.FilePath))
                {
                    var buffer = new byte[stream.Length];
                    stream.Read(buffer, 0, buffer.Length);

                    memoryStream = new MemoryStream(buffer);
                }

                entity.Stream = memoryStream;

                // here we prepare the Uri to allow the file to be
                // accessed directly, without using the download handler
                var rootUri = this.LoaderRootUrl();
                var location = string.Concat(
                    rootUri.AbsoluteUri.TrimEnd('/'),
                    "/",
                    entity.Id,
                    Path.GetExtension(entity.OriginalName));

                if (!Uri.IsWellFormedUriString(location, UriKind.Absolute))
                {
                    throw new Domain.Exceptions.ApplicationException(
                        string.Concat(
                            "We could not create a valid absolute Uri with the information ",
                            "saved for the entity. The path is '",
                            location,
                            "'"));
                }

                entity.Uri = new Uri(location, UriKind.Absolute);
                return entity;
            }
        }

        /// <summary>
        /// There is a limit of files that can be placed in the same folder.
        /// That happens for any file system. We therefore divide the files
        /// accross folders. This method provides a folder name created out
        /// of the file Id (first chars of each Guid char group).
        /// </summary>
        /// <param name="fileId">The id of the file.</param>
        /// <returns>Returns a folder name from the file id.</returns>
        private static string GetBaseFolderForId(Guid fileId)
        {
            var parsedId = fileId.ToString();
            var frags = parsedId.Split('-');

            var folderName = new StringBuilder();
            foreach (var part in frags)
            {
                folderName.Append(part.FirstOrDefault());
            }

            return folderName.ToString();
        }

        /// <summary>
        /// Creates a new file or modifies an existing file.
        /// </summary>
        /// <param name="path">The path of the file.</param>
        /// <param name="stream">The stream of the file.</param>
        private static void CreateOrModifyFile(
            string path,
            Stream stream)
        {
            // now, if the file exists, we delete it before we save the new file
            var file = new FileInfo(path);
            if (file.Exists)
            {
                if (file.IsReadOnly)
                {
                    file.IsReadOnly = false;
                }

                file.Delete();
            }

            using (var fileStream = file.OpenWrite())
            {
                var buffer = new byte[stream.Length];
                stream.Read(buffer, 0, buffer.Length);
                fileStream.Write(buffer, 0, buffer.Length);
            }
        }

        /// <summary>
        /// Adds a database entry (or updates an existing entry) related
        /// to this blob, to improve queryability and integrity.
        /// </summary>
        /// <param name="filePath">The filePath that will increase queryability.</param>
        /// <param name="entity">The entity of the blob from which data will be saved.</param>
        private void AddOrUpdateDatabaseEntry(
            string filePath,
            Blob entity)
        {
            using (var context = NewContext())
            {
                var blob = context.Blobs
                    .Where(d => d.Id == entity.Id)
                    .FirstOrDefault();

                if (blob == null)
                {
                    blob = new Core.Blob();
                    context.Blobs.AddObject(blob);

                    // setting readonly properties and properties that
                    // do not exist in the database blob entry.
                    blob.Id = entity.Id;
                    blob.FilePath = filePath;
                }

                CopyFromDomainToEF4<Blob, Core.Blob>(
                    entity,
                    blob);

                context.SaveChanges();
            }
        }
    }
}


